﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;

namespace SLRT.dao.data
{
    public class Shape: ICloneable, IComparable
    {
        private HashSet<Point?> set;
        private int sumX;
        private int sumY;

        private Point? leftMostPoint;
        private Point? rightMostPoint;
        private Point? topMostPoint;
        private Point? bottomMostPoint;

        public Shape()
        {
            this.set = new HashSet<Point?>();
            this.sumX = 0;
            this.sumY = 0;
            this.leftMostPoint = null;
            this.rightMostPoint = null;
            this.topMostPoint = null;
            this.bottomMostPoint = null;
        }

        public Shape(HashSet<Point?> set, int sumX, int sumY, Point? leftMostPoint, Point? rightMostPoint, Point? topMostPoint, Point? bottomMostPoint)
        {
            this.set = set;
            this.sumX = sumX;
            this.sumY = sumY;
            this.leftMostPoint = leftMostPoint;
            this.rightMostPoint = rightMostPoint;
            this.topMostPoint = topMostPoint;
            this.bottomMostPoint = bottomMostPoint;
        }

        public void Add(Point? p)
        {
            int x = p.Value.X;
            int y = p.Value.Y;

            this.set.Add(p);

            this.sumX += x;
            this.sumY += y;

            if (this.leftMostPoint == null)
            {
                this.leftMostPoint = p;
            }
            else
            {
                if (x < this.leftMostPoint.Value.X)
                {
                    this.leftMostPoint = p;
                }
            }

            if (this.rightMostPoint == null)
            {
                this.rightMostPoint = p;
            }
            else
            {
                if (x > this.rightMostPoint.Value.X)
                {
                    this.rightMostPoint = p;
                }
            }

            if (this.topMostPoint == null)
            {
                this.topMostPoint = p;
            }
            else
            {
                if (y < this.topMostPoint.Value.Y)
                {
                    this.topMostPoint = p;
                }
            }

            if (this.bottomMostPoint == null)
            {
                this.bottomMostPoint = p;
            }
            else
            {
                if (y > this.bottomMostPoint.Value.Y)
                {
                    this.bottomMostPoint = p;
                }
            }
        }

        public void Add(Shape shape)
        {
            if (this.bottomMostPoint.Value.Y < shape.BottomMostPoint.Value.Y)
            {
                this.bottomMostPoint = shape.BottomMostPoint;
            }
            if (this.topMostPoint.Value.Y > shape.TopMostPoint.Value.Y)
            {
                this.topMostPoint = shape.TopMostPoint;
            }
            if (this.leftMostPoint.Value.X > shape.LeftMostPoint.Value.X)
            {
                this.leftMostPoint = shape.LeftMostPoint;
            }
            if (this.rightMostPoint.Value.X < shape.RightMostPoint.Value.X)
            {
                this.rightMostPoint = shape.RightMostPoint;
            }

            this.Set.UnionWith(shape.Set);
        }

        public void remove(Point? p)
        {
            int x = p.Value.X;
            int y = p.Value.Y;

            this.set.Remove(p);

            this.sumX -= x;
            this.sumY -= y;

            // BIG WARNING! Margin points and coords are not moved back.
        }

        public void subtract(Shape s)
        {
            int area = this.set.Count;
            
            Point? center = s.getCenter();
            
            int cx = center.Value.X;
            int cy = center.Value.Y;

            foreach (Point? p in s.set)
            {
                this.set.Remove(p);
            }

            this.sumX -= area * cx;
            this.sumY -= area * cy;

            // BIG WARNING! Margin points and coords are not moved back.
        }

        public Point? getCenter()
        {
            int s = this.set.Count;
            if (s != 0)
            {
                return new Point((int)Math.Round((float)sumX / s), (int)Math.Round((float)sumY / s));
            }

            return null;
        }

        public Point? LeftMostPoint
        {
            get { return this.leftMostPoint; }
        }

        public Point? RightMostPoint
        {
            get { return this.rightMostPoint; }
        }

        public Point? TopMostPoint
        {
            get { return this.topMostPoint; }
        }

        public Point? BottomMostPoint
        {
            get { return this.bottomMostPoint; }
        }

        public int Area
        {
            get { return this.set.Count; }
        }

        /*
         * This can be immensely optimized. From O(n*n) to O(n).
         * And really, this is the part where we loose time.
         * Just a little mathematics. Later on...
         */
        public double getElongation()
        {
            Point? ce = this.getCenter();
            int px, py, dx, dy, cx = ce.Value.X, cy = ce.Value.Y;
            int nom = 0, den = 0;
            double result = 0.0;

            foreach (Point? p in this.set)
            {
                px = p.Value.X;
                py = p.Value.Y;
                dy = (cy - py);
                dx = (cx - px);
                nom += dx * dy;
                den += dx * dx - dy * dy;
            }

            if (den != 0)
            {
                result = Math.Atan(2.0 * nom / den);
                result /= 2;
                if (result > 0)
                {
                    result -= Math.PI / 2;
                }
                else
                {
                    result += Math.PI / 2;
                }
            }

            return result;
        }

        public double getAspectRatio()
        {
            double el = this.getElongation();
            double le = this.getElongation() + Math.PI / 2;
            double tanel = Math.Tan(el), tanle = Math.Tan(le), cosel = Math.Cos(el);
            double minX, maxX, minY, maxY;
            double width, height, ratio = 0.0;
            double x, y;
            Point? pt = this.set.ElementAt(0);

            minY = maxY = pt.Value.Y - tanel * pt.Value.X;
            minX = maxX = (pt.Value.X - pt.Value.Y) / tanle;

            foreach (Point? p in this.set.Skip(1))
            {
                // elongation parallel intersects the OY axis in:
                y = p.Value.Y - tanel * p.Value.X;

                // elongation perpendicular intersects the OX axis in:
                x = (p.Value.X - p.Value.Y) / tanle;

                if (y > maxY)
                {
                    maxY = y;
                }
                if (y < minY)
                {
                    minY = y;
                }
                if (x > maxX)
                {
                    maxX = x;
                }
                if (x < minX)
                {
                    minX = x;
                }
            }


            // length by perpendicular
            width = Math.Abs((maxY - minY) * cosel);

            // length by elongation
            height = Math.Abs((maxX - minX) * cosel);

            if ((height != 0) && (width != 0))
            {
                if (width > height)
                {
                    ratio = width / height;
                }
                else
                {
                    ratio = height / width;
                }
            }

            return ratio;
        }

        public Point? getPoint(int k)
        {
            return this.set.ElementAt(k);
        }


        public bool contains(Point? p)
        {
            return this.set.Contains(p);
        }

        public HashSet<Point?> Set
        {
            get { return this.set; }
        }

        public object Clone()
        {
            HashSet<Point?> clonedSet = new HashSet<Point?>(this.set);
              
            Shape clonedShape = new Shape(clonedSet, this.sumX, this.sumY, this.leftMostPoint, this.rightMostPoint, this.topMostPoint, this.bottomMostPoint);

            return clonedShape;
        }

        public int CompareTo(Object o)
        {
            if (o.GetType() == this.GetType()) {
                Shape shape = (Shape) o;
                if (shape.Area > this.Area)
                {
                    return -1;
                }
                else if (shape.Area < this.Area)
                {
                    return 1;
                }
                else
                {
                    return 0;
                }
            }

            return 0;
        }
    }
}
